"""
DjangoBlog 视图模块
处理博客系统的所有页面请求，包括文章列表、详情、分类、标签等功能
基于Django的类视图和函数视图实现
"""

import logging
import os
import uuid

from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView # type: ignore

from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.utils import cache, get_blog_setting, get_sha256

# 配置日志记录器，用于记录视图层的操作日志
logger = logging.getLogger(__name__)


class ArticleListView(ListView):
    """
    文章列表基类视图
    提供文章列表的通用功能，包括缓存、分页等
    其他列表视图都继承自此类
    """
    
    # 指定渲染模板
    template_name = 'blog/article_index.html'
    
    # 上下文变量名，在模板中使用article_list访问文章列表
    context_object_name = 'article_list'
    
    # 页面类型描述，用于在页面中显示
    page_type = ''
    
    # 每页显示的文章数量，从settings中获取
    paginate_by = settings.PAGINATE_BY
    
    # 分页参数名，在URL中使用
    page_kwarg = 'page'
    
    # 友情链接显示类型
    link_type = LinkShowType.L

    def get_view_cache_key(self):
        """获取视图缓存键，子类可重写此方法"""
        return self.request.GET['pages']

    @property
    def page_number(self):
        """
        计算当前页码的属性方法
        优先级：URL参数 > GET参数 > 默认第一页
        """
        page_kwarg = self.page_kwarg
        page = self.kwargs.get(
            page_kwarg) or self.request.GET.get(page_kwarg) or 1
        return page

    def get_queryset_cache_key(self):
        """
        抽象方法 - 获取查询集的缓存键
        子类必须实现此方法，返回用于缓存查询结果的键名
        """
        raise NotImplementedError()

    def get_queryset_data(self):
        """
        抽象方法 - 获取查询集数据
        子类必须实现此方法，返回实际的查询集数据
        """
        raise NotImplementedError()

    def get_queryset_from_cache(self, cache_key):
        """
        从缓存获取查询集数据
        如果缓存不存在，则从数据库获取并设置缓存
        
        参数:
            cache_key: 缓存键名
            
        返回:
            article_list: 文章列表数据
        """
        value = cache.get(cache_key)  # 尝试从缓存获取数据
        if value:
            # 缓存命中，记录日志并返回缓存数据
            logger.info('get view cache.key:{key}'.format(key=cache_key))
            return value
        else:
            # 缓存未命中，从数据库查询数据
            article_list = self.get_queryset_data()
            cache.set(cache_key, article_list)  # 设置缓存
            logger.info('set view cache.key:{key}'.format(key=cache_key))
            return article_list

    def get_queryset(self):
        """
        重写父类方法，实现缓存机制
        先从缓存获取数据，如果不存在则从数据库查询
        """
        key = self.get_queryset_cache_key()  # 获取缓存键
        value = self.get_queryset_from_cache(key)  # 从缓存获取数据
        return value

    def get_context_data(self, **kwargs):
        """
        添加上下文数据
        将链接类型添加到模板上下文
        """
        kwargs['linktype'] = self.link_type
        return super(ArticleListView, self).get_context_data(**kwargs)


class IndexView(ArticleListView):
    """
    博客首页视图
    显示最新的文章列表，支持分页显示
    """
    
    # 首页显示的友情链接类型
    link_type = LinkShowType.I

    def get_queryset_data(self):
        """
        获取首页文章数据
        只返回已发布的普通文章（非页面）
        """
        article_list = Article.objects.filter(type='a', status='p')
        return article_list

    def get_queryset_cache_key(self):
        """
        生成首页缓存键
        根据页码生成不同的缓存键，实现分页缓存
        """
        cache_key = 'index_{page}'.format(page=self.page_number)
        return cache_key


class ArticleDetailView(DetailView):
    """
    文章详情页视图
    显示单篇文章的完整内容，包括评论等
    """
    
    # 详情页模板
    template_name = 'blog/article_detail.html'
    
    # 使用的数据模型
    model = Article
    
    # URL中的文章ID参数名
    pk_url_kwarg = 'article_id'
    
    # 上下文变量名，在模板中使用article访问文章对象
    context_object_name = "article"

    def get_object(self, queryset=None):
        """
        获取文章对象并增加浏览量
        重写父类方法，在获取对象时自动增加文章浏览量
        """
        obj = super(ArticleDetailView, self).get_object()  # 调用父类方法获取对象
        obj.viewed()  # 增加文章浏览量
        self.object = obj  # 设置对象实例变量
        return obj

    def get_context_data(self, **kwargs):
        """
        添加上下文数据
        包括评论表单、评论列表、上下篇文章等
        """
        # 创建评论表单实例
        comment_form = CommentForm()

        # 获取文章的所有有效评论
        article_comments = self.object.comment_list()
        
        # 筛选出顶级评论（没有父评论的评论）
        parent_comments = article_comments.filter(parent_comment=None)
        
        # 获取博客设置
        blog_setting = get_blog_setting()
        
        # 对顶级评论进行分页
        paginator = Paginator(parent_comments, blog_setting.article_comment_count)
        
        # 获取评论页码参数，默认为第一页
        page = self.request.GET.get('comment_page', '1')
        
        # 页码验证和转换
        if not page.isnumeric():
            page = 1  # 如果页码不是数字，默认第一页
        else:
            page = int(page)  # 转换为整数
            if page < 1:
                page = 1  # 页码不能小于1
            if page > paginator.num_pages:
                page = paginator.num_pages  # 页码不能大于总页数

        # 获取指定页码的评论
        p_comments = paginator.page(page)
        
        # 计算下一页和上一页的页码
        next_page = p_comments.next_page_number() if p_comments.has_next() else None
        prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None

        # 生成下一页和上一页的URL（带锚点）
        if next_page:
            kwargs[
                'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
        if prev_page:
            kwargs[
                'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
        
        # 将数据添加到上下文
        kwargs['form'] = comment_form  # 评论表单
        kwargs['article_comments'] = article_comments  # 所有评论
        kwargs['p_comments'] = p_comments  # 当前页的评论
        kwargs['comment_count'] = len(
            article_comments) if article_comments else 0  # 评论总数

        # 添加上下一篇推荐文章
        kwargs['next_article'] = self.object.next_article
        kwargs['prev_article'] = self.object.prev_article

        return super(ArticleDetailView, self).get_context_data(**kwargs)


class CategoryDetailView(ArticleListView):
    """
    分类目录详情页视图
    显示指定分类下的所有文章
    """
    
    page_type = "分类目录归档"  # 页面类型描述

    def get_queryset_data(self):
        """
        获取分类下的文章数据
        包括当前分类及其所有子分类的文章
        """
        slug = self.kwargs['category_name']  # 从URL获取分类slug
        category = get_object_or_404(Category, slug=slug)  # 获取分类对象，不存在则404

        categoryname = category.name  # 分类名称
        self.categoryname = categoryname  # 保存到实例变量
        
        # 获取分类的所有子分类名称（包括自身）
        categorynames = list(
            map(lambda c: c.name, category.get_sub_categorys()))
        
        # 查询这些分类下的所有已发布文章
        article_list = Article.objects.filter(
            category__name__in=categorynames, status='p')
        return article_list

    def get_queryset_cache_key(self):
        """
        生成分类页缓存键
        基于分类名称和页码生成缓存键
        """
        slug = self.kwargs['category_name']
        category = get_object_or_404(Category, slug=slug)
        categoryname = category.name
        self.categoryname = categoryname
        cache_key = 'category_list_{categoryname}_{page}'.format(
            categoryname=categoryname, page=self.page_number)
        return cache_key

    def get_context_data(self, **kwargs):
        """
        添加上下文数据
        包括页面类型和分类名称
        """
        categoryname = self.categoryname
        try:
            # 尝试从分类路径中提取最后一级名称
            categoryname = categoryname.split('/')[-1]
        except BaseException:
            # 如果提取失败，保持原名称
            pass
        
        kwargs['page_type'] = CategoryDetailView.page_type  # 页面类型
        kwargs['tag_name'] = categoryname  # 分类名称（用于页面显示）
        return super(CategoryDetailView, self).get_context_data(**kwargs)


class AuthorDetailView(ArticleListView):
    """
    作者详情页视图
    显示指定作者的所有文章
    """
    
    page_type = '作者文章归档'  # 页面类型描述

    def get_queryset_cache_key(self):
        """
        生成作者页缓存键
        基于作者名称和页码生成缓存键
        """
        from uuslug import slugify # type: ignore
        author_name = slugify(self.kwargs['author_name'])  # 对作者名进行slugify处理
        cache_key = 'author_{author_name}_{page}'.format(
            author_name=author_name, page=self.page_number)
        return cache_key

    def get_queryset_data(self):
        """
        获取作者的文章数据
        只返回已发布的普通文章
        """
        author_name = self.kwargs['author_name']  # 从URL获取作者用户名
        article_list = Article.objects.filter(
            author__username=author_name, type='a', status='p')  # 查询该作者的文章
        return article_list

    def get_context_data(self, **kwargs):
        """
        添加上下文数据
        包括页面类型和作者名称
        """
        author_name = self.kwargs['author_name']  # 作者名称
        kwargs['page_type'] = AuthorDetailView.page_type  # 页面类型
        kwargs['tag_name'] = author_name  # 作者名称（用于页面显示）
        return super(AuthorDetailView, self).get_context_data(**kwargs)


class TagDetailView(ArticleListView):
    """
    标签详情页视图
    显示指定标签下的所有文章
    """
    
    page_type = '分类标签归档'  # 页面类型描述

    def get_queryset_data(self):
        """
        获取标签下的文章数据
        返回带有指定标签的所有已发布文章
        """
        slug = self.kwargs['tag_name']  # 从URL获取标签slug
        tag = get_object_or_404(Tag, slug=slug)  # 获取标签对象，不存在则404
        tag_name = tag.name  # 标签名称
        self.name = tag_name  # 保存到实例变量
        
        # 查询带有该标签的所有已发布文章
        article_list = Article.objects.filter(
            tags__name=tag_name, type='a', status='p')
        return article_list

    def get_queryset_cache_key(self):
        """
        生成标签页缓存键
        基于标签名称和页码生成缓存键
        """
        slug = self.kwargs['tag_name']
        tag = get_object_or_404(Tag, slug=slug)
        tag_name = tag.name
        self.name = tag_name
        cache_key = 'tag_{tag_name}_{page}'.format(
            tag_name=tag_name, page=self.page_number)
        return cache_key

    def get_context_data(self, **kwargs):
        """
        添加上下文数据
        包括页面类型和标签名称
        """
        tag_name = self.name  # 标签名称
        kwargs['page_type'] = TagDetailView.page_type  # 页面类型
        kwargs['tag_name'] = tag_name  # 标签名称（用于页面显示）
        return super(TagDetailView, self).get_context_data(**kwargs)


class ArchivesView(ArticleListView):
    """
    文章归档页视图
    显示所有文章的按时间归档，不分页
    """
    
    page_type = '文章归档'  # 页面类型描述
    paginate_by = None  # 不分页
    page_kwarg = None  # 无分页参数
    template_name = 'blog/article_archives.html'  # 归档页专用模板

    def get_queryset_data(self):
        """
        获取所有已发布文章
        用于生成完整的归档列表
        """
        return Article.objects.filter(status='p').all()

    def get_queryset_cache_key(self):
        """
        生成归档页缓存键
        归档页只有一个缓存键，因为不分页
        """
        cache_key = 'archives'
        return cache_key


class LinkListView(ListView):
    """
    友情链接页视图
    显示所有启用的友情链接
    """
    
    model = Links  # 使用的数据模型
    template_name = 'blog/links_list.html'  # 链接页模板

    def get_queryset(self):
        """
        获取已启用的友情链接
        只返回is_enable=True的链接
        """
        return Links.objects.filter(is_enable=True)


class EsSearchView(SearchView):
    """
    搜索引擎视图
    基于Haystack实现文章搜索功能
    """
    
    def get_context(self):
        """
        构建搜索结果的上下文
        包括查询词、分页、建议词等
        """
        # 构建分页
        paginator, page = self.build_page()
        
        # 构建基础上下文
        context = {
            "query": self.query,  # 搜索关键词
            "form": self.form,  # 搜索表单
            "page": page,  # 当前页码数据
            "paginator": paginator,  # 分页器
            "suggestion": None,  # 搜索建议
        }
        
        # 如果有拼写建议，添加到上下文
        if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
            context["suggestion"] = self.results.query.get_spelling_suggestion()
        
        # 添加额外上下文
        context.update(self.extra_context())

        return context


@csrf_exempt  # 免除CSRF验证，用于文件上传
def fileupload(request):
    """
    文件上传视图函数
    提供图床功能，支持图片和其他文件的上传
    需要调用端提供签名验证
    
    参数:
        request: HTTP请求对象
        
    返回:
        HttpResponse: 上传结果或错误信息
    """
    if request.method == 'POST':
        # 获取签名参数，用于安全验证
        sign = request.GET.get('sign', None)
        if not sign:
            return HttpResponseForbidden()  # 无签名，拒绝访问
        
        # 验证签名是否正确
        if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
            return HttpResponseForbidden()  # 签名错误，拒绝访问
        
        response = []  # 存储上传成功的文件URL
        
        # 遍历所有上传的文件
        for filename in request.FILES:
            # 生成基于日期的目录路径
            timestr = timezone.now().strftime('%Y/%m/%d')
            
            # 图片扩展名列表，用于判断文件类型
            imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
            fname = u''.join(str(filename))
            
            # 判断是否为图片文件
            isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
            
            # 构建保存目录：图片和其他文件分开存放
            base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
            
            # 如果目录不存在则创建
            if not os.path.exists(base_dir):
                os.makedirs(base_dir)
            
            # 生成唯一的文件名，避免重名
            savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
            
            # 安全检查：确保保存路径在预期目录内
            if not savepath.startswith(base_dir):
                return HttpResponse("only for post")
            
            # 保存文件到服务器
            with open(savepath, 'wb+') as wfile:
                for chunk in request.FILES[filename].chunks():
                    wfile.write(chunk)
            
            # 如果是图片，进行压缩优化
            if isimage:
                from PIL import Image
                image = Image.open(savepath)
                image.save(savepath, quality=20, optimize=True)  # 质量设置为20%，优化存储
            
            # 生成文件的静态URL
            url = static(savepath)
            response.append(url)  # 添加到响应列表
        
        return HttpResponse(response)  # 返回上传成功的文件URL列表

    else:
        # 非POST请求，返回错误信息
        return HttpResponse("only for post")


def page_not_found_view(
        request,
        exception,
        template_name='blog/error_page.html'):
    """
    404页面未找到错误处理视图
    
    参数:
        request: 请求对象
        exception: 异常信息
        template_name: 错误页面模板
        
    返回:
        HttpResponse: 渲染的错误页面
    """
    if exception:
        logger.error(exception)  # 记录异常日志
    
    url = request.get_full_path()  # 获取请求的完整URL
    
    # 渲染404错误页面
    return render(request,
                  template_name,
                  {'message': _('抱歉，您请求的页面未找到，请点击首页查看其他内容？'),
                   'statuscode': '404'},  # 错误消息和状态码
                  status=404)  # HTTP状态码


def server_error_view(request, template_name='blog/error_page.html'):
    """
    500服务器错误处理视图
    
    参数:
        request: 请求对象
        template_name: 错误页面模板
        
    返回:
        HttpResponse: 渲染的错误页面
    """
    # 渲染500错误页面
    return render(request,
                  template_name,
                  {'message': _('抱歉，服务器繁忙，请点击首页查看其他内容？'),
                   'statuscode': '500'},  # 错误消息和状态码
                  status=500)  # HTTP状态码


def permission_denied_view(
        request,
        exception,
        template_name='blog/error_page.html'):
    """
    403权限拒绝错误处理视图
    
    参数:
        request: 请求对象
        exception: 异常信息
        template_name: 错误页面模板
        
    返回:
        HttpResponse: 渲染的错误页面
    """
    if exception:
        logger.error(exception)  # 记录异常日志
    
    # 渲染403错误页面
    return render(
        request, template_name, {
            'message': _('抱歉，您没有权限访问此页面？'),
            'statuscode': '403'},  # 错误消息和状态码
        status=403)  # HTTP状态码


def clean_cache_view(request):
    """
    清理缓存视图
    用于手动清除所有缓存，通常在配置修改后调用
    
    参数:
        request: 请求对象
        
    返回:
        HttpResponse: 清理结果
    """
    cache.clear()  # 清除所有缓存
    return HttpResponse('ok')  # 返回成功信息